
/*-------------------------------------------------------------------------
 *
 * sync.c
 *	  文件同步管理代码。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/storage/sync/sync.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <unistd.h>
#include <fcntl.h>
#include <sys/file.h>

#include "access/commit_ts.h"
#include "access/clog.h"
#include "access/multixact.h"
#include "access/xlog.h"
#include "access/xlogutils.h"
#include "commands/tablespace.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "portability/instr_time.h"
#include "postmaster/bgwriter.h"
#include "storage/bufmgr.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/latch.h"
#include "storage/md.h"
#include "utils/hsearch.h"
#include "utils/inval.h"
#include "utils/memutils.h"

static MemoryContext pendingOpsCxt; /* 待处理操作状态的上下文 */

/*
 * 在某些上下文（当前是独立后端和检查点进程）中，我们跟踪待处理的 fsync 操作：我们需要记住自上一个检查点以来已写入的所有关系段，以便在完成下一个检查点之前将它们 fsync 到磁盘。 此哈希表记住待处理操作。 我们主要使用哈希表作为合并重复请求的方便方式。
 *
 * 我们使用类似的机制来记住不再需要的文件，这些文件可以在下一个检查点后被删除，但我们使用链表而不是哈希表，因为我们不期望有任何重复请求。
 *
 * 这些机制仅用于非临时关系；我们从不 fsync 临时关系，也不需要推迟它们的删除（参见 mdunlink 中的注释）。
 *
 * （普通后端不会在本地跟踪待处理操作，而是将其转发给检查点进程。）
 */
typedef uint16 CycleCtr;		/* 可以是任何方便的整数大小 */

typedef struct
{
	FileTag		tag;			/* 标识处理程序和文件 */
	CycleCtr	cycle_ctr;		/* 最旧请求的 sync_cycle_ctr */
	bool		canceled;		/* 如果我们“最近”取消了，则 canceled 为 true */
} PendingFsyncEntry;

typedef struct
{
	FileTag		tag;			/* 标识处理程序和文件 */
	CycleCtr	cycle_ctr;		/* 请求发出时的 checkpoint_cycle_ctr */
	bool		canceled;		/* 如果请求已被取消则为 true */
} PendingUnlinkEntry;

static HTAB *pendingOps = NULL;
static List *pendingUnlinks = NIL;
static MemoryContext pendingOpsCxt; /* 上述内容的上下文 */

static CycleCtr sync_cycle_ctr = 0;
static CycleCtr checkpoint_cycle_ctr = 0;

/* 调用 AbsorbSyncRequests 的间隔 */
#define FSYNCS_PER_ABSORB		10
#define UNLINKS_PER_ABSORB		10

/*
 * 处理同步和取消链接请求的函数指针。
 */
typedef struct SyncOps
{
	int			(*sync_syncfiletag) (const FileTag *ftag, char *path);
	int			(*sync_unlinkfiletag) (const FileTag *ftag, char *path);
	bool		(*sync_filetagmatches) (const FileTag *ftag,
										const FileTag *candidate);
} SyncOps;

/*
 * 这些索引必须与 SyncRequestHandler 枚举的值相对应。
 */
static const SyncOps syncsw[] = {
	/* 磁盘 */
	[SYNC_HANDLER_MD] = {
		.sync_syncfiletag = mdsyncfiletag,
		.sync_unlinkfiletag = mdunlinkfiletag,
		.sync_filetagmatches = mdfiletagmatches
	},
	/* pg_xact */
	[SYNC_HANDLER_CLOG] = {
		.sync_syncfiletag = clogsyncfiletag
	},
	/* pg_commit_ts */
	[SYNC_HANDLER_COMMIT_TS] = {
		.sync_syncfiletag = committssyncfiletag
	},
	/* pg_multixact/offsets */
	[SYNC_HANDLER_MULTIXACT_OFFSET] = {
		.sync_syncfiletag = multixactoffsetssyncfiletag
	},
	/* pg_multixact/members */
	[SYNC_HANDLER_MULTIXACT_MEMBER] = {
		.sync_syncfiletag = multixactmemberssyncfiletag
	}
};

/*
 * 初始化文件同步跟踪的数据结构。
 */
void InitSync(void)
{
	/*
	 * 如果需要，创建待处理操作哈希表。目前，我们需要它
	 * 如果我们是独立的（不在 postmaster 下）或如果我们是
	 * 检查点辅助进程。
	 */
	if (!IsUnderPostmaster || AmCheckpointerProcess())
	{
		HASHCTL		fc_hash_ctl;

		/*
		 * XXX：检查点需要在吸收 fsync 请求时向待处理操作表
		 * 添加条目。这是在临界区内完成的，通常不允许这样做，
		 * 但我们破例。这意味着在吸收 fsync 请求时，你有理论上
		 * 可能耗尽内存的风险，这会导致 PANIC。
		 * 幸运的是，哈希表很小，因此在实践中不太可能发生这种情况。
		 */
		pendingOpsCxt = AllocSetContextCreate(TopMemoryContext,
											  "Pending ops context",
											  ALLOCSET_DEFAULT_SIZES);
		MemoryContextAllowInCriticalSection(pendingOpsCxt, true);

		fc_hash_ctl.keysize = sizeof(FileTag);
		fc_hash_ctl.entrysize = sizeof(PendingFsyncEntry);
		fc_hash_ctl.hcxt = pendingOpsCxt;
		pendingOps = hash_create("Pending Ops Table",
								 100L,
								 &fc_hash_ctl,
								 HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
		pendingUnlinks = NIL;
	}
}

/*
 * SyncPreCheckpoint() -- 做预检查点工作
 *
 * 为了区分在此检查点之前到达的取消链接请求和
 * 在检查点期间到达的请求，我们使用一个与 fsync 请求
 * 相似的循环计数器。此循环计数器在此处递增。
 *
 * 这必须在确定检查点 REDO 点之前调用。
 * 这确保我们不会过早删除文件。由于此调用
 * AbsorbSyncRequests()，它执行内存分配，因此不能在
 * 临界区内调用。
 *
 * 请注意，我们无法在这里做任何依赖于假设
 * 检查点会完成的事情。
 */
void SyncPreCheckpoint(void)
{
	/*
	 * 诸如 DROP TABLESPACE 之类的操作假设下一个检查点将
	 * 处理所有最近转发的取消链接请求，但如果在
	 * 递增循环计数器之前没有被吸收，它们将在
	 * 下一个检查点之后才会被处理。以下吸收确保
	 * 在检查点开始之前转发的任何取消链接请求将在
	 * 当前检查点中被处理。
	 */
	AbsorbSyncRequests();

	/*
	 * 在这一点到达的任何取消链接请求将被分配下一个
	 * 循环计数器，并且不会在下一个检查点之前被取消链接。
	 */
	checkpoint_cycle_ctr++;
}

/*
 * SyncPostCheckpoint() -- 做后检查点工作
 *
 * 删除现在可以安全删除的任何残留文件。
 */
void SyncPostCheckpoint(void)
{
	int			fc_absorb_counter;
	ListCell   *fc_lc;

	fc_absorb_counter = UNLINKS_PER_ABSORB;
	foreach(fc_lc, pendingUnlinks)
	{
		PendingUnlinkEntry *fc_entry = (PendingUnlinkEntry *) lfirst(fc_lc);
		char		fc_path[MAXPGPATH];

		/* 跳过任何已取消的条目 */
		if (fc_entry->canceled)
			continue;

		/*
		 * 新条目附加到末尾，因此如果条目是新的，我们
		 * 已经到达旧条目的末尾。
		 *
		 * 注意：如果恰好有连续的检查点失败，我们
		 * 可能会被循环计数器的环绕所愚弄。然而，唯一的
		 * 后果是我们将延迟取消链接一个检查点，
		 * 这是完全可以接受的。
		 */
		if (fc_entry->cycle_ctr == checkpoint_cycle_ctr)
			break;

		/* 取消链接文件 */
		if (syncsw[fc_entry->tag.handler].sync_unlinkfiletag(&fc_entry->tag,
														  fc_path) < 0)
		{
			/*
			 * 在处理待处理的取消链接请求时存在竞争条件，
			 * 当数据库被删除的同时。如果
			 * DROP DATABASE 在我们进行取消链接之前已删除文件，
			 * 我们将在这里得到 ENOENT。rmtree() 也必须忽略
			 * ENOENT 错误，以处理我们首先删除文件的可能性。
			 */
			if (errno != ENOENT)
				ereport(WARNING,
						(errcode_for_file_access(),
						 errmsg("could not remove file \"%s\": %m", fc_path)));
		}

		/* 将列表条目标记为已取消，以防万一 */
		fc_entry->canceled = true;

		/*
		 * 与 ProcessSyncRequests 中一样，当有许多删除
		 * 要执行时，我们不想长时间停止吸收 fsync 请求。
		 * 在循环的这一点，我们可以安全地调用
		 * AbsorbSyncRequests()。
		 */
		if (--fc_absorb_counter <= 0)
		{
			AbsorbSyncRequests();
			fc_absorb_counter = UNLINKS_PER_ABSORB;
		}
	}

	/*
	 * 如果我们到达列表的末尾，我们可以直接删除整个列表
	 * （记得 pfree 所有 PendingUnlinkEntry 对象）。否则，
	 * 我们必须保留“lc”处或之后的条目。
	 */
	if (fc_lc == NULL)
	{
		list_free_deep(pendingUnlinks);
		pendingUnlinks = NIL;
	}
	else
	{
		int			fc_ntodelete = list_cell_number(pendingUnlinks, fc_lc);

		for (int fc_i = 0; fc_i < fc_ntodelete; fc_i++)
			pfree(list_nth(pendingUnlinks, fc_i));

		pendingUnlinks = list_delete_first_n(pendingUnlinks, fc_ntodelete);
	}
}

/*
 *	ProcessSyncRequests() -- 处理排队的 fsync 请求。
 */
void ProcessSyncRequests(void)
{
	static bool fc_sync_in_progress = false;

	HASH_SEQ_STATUS fc_hstat;
	PendingFsyncEntry *fc_entry;
	int			fc_absorb_counter;

	/* 同步时间的统计信息 */
	int			fc_processed = 0;
	instr_time	fc_sync_start,
				fc_sync_end,
				fc_sync_diff;
	uint64		fc_elapsed;
	uint64		fc_longest = 0;
	uint64		fc_total_elapsed = 0;

	/*
	 * 这仅在检查点期间调用，检查点应仅在创建
	 * 待处理操作的进程中发生。
	 */
	if (!pendingOps)
		elog(ERROR, "cannot sync without a pendingOps table");

	/*
	 * 如果我们在检查点功能中，则同步最好包括所有由后端排队的
	 * fsync请求，直到这一点。最紧迫的
	 * 竞争条件是，必须为检查点写入和fsync的缓冲区可能在被BufferSync()访问之前
	 * 已被后端转储。我们知道后端在清除缓冲区的dirtybit之前将排队一个fsync请求，所以
	 * 只要在完成BufferSync()后进行一次吸收，我们就安全了。
	 */
	AbsorbSyncRequests();

	/*
	 * 为了避免过多的fsync（在最糟糕的情况下，可能是一个永不结束的
	 * 检查点），我们希望忽略在此之后进入哈希表的fsync请求——它们
	 * 应该在下次处理，而不是现在。我们使用sync_cycle_ctr来
	 * 区分旧条目和新条目：新条目的cycle_ctr等于sync_cycle_ctr
	 * 的增量值。
	 *
	 * 在正常情况下，此时表中所有存在的条目都将具有cycle_ctr
	 * 恰好等于当前（即将变为旧）值的sync_cycle_ctr。然而，如果
	 * 我们在fsync循环的过程中失败，那么旧的cycle_ctr值可能会
	 * 在我们再次尝试时保留。重复的检查点失败最终将使计数器
	 * 回绕到一个旧条目可能看起来是新的，从而导致我们跳过它，
	 * 可能让一个本不该成功的检查点成功。为了防止回绕，每次
	 * 上一次ProcessSyncRequests()未能完成时，扫描表并强制
	 * 设置cycle_ctr = sync_cycle_ctr。
	 *
	 * 请不要将此循环与主循环合并，因为问题恰恰在于该循环可能会
	 * 在访问所有条目之前失败。从性能的角度来看，这条路径在
	 * 正常运作的系统中永远不会被走到。
	 */
	if (fc_sync_in_progress)
	{
		/* 上一次尝试失败，因此更新任何过时的cycle_ctr值 */
		hash_seq_init(&fc_hstat, pendingOps);
		while ((fc_entry = (PendingFsyncEntry *) hash_seq_search(&fc_hstat)) != NULL)
		{
			fc_entry->cycle_ctr = sync_cycle_ctr;
		}
	}

	/* 前进计数器，以便新的哈希表条目可以区分 */
	sync_cycle_ctr++;

	/* 设置标志以检测如果我们没有到达循环的末尾就失败 */
	fc_sync_in_progress = true;

	/* 现在扫描哈希表以处理fsync请求 */
	fc_absorb_counter = FSYNCS_PER_ABSORB;
	hash_seq_init(&fc_hstat, pendingOps);
	while ((fc_entry = (PendingFsyncEntry *) hash_seq_search(&fc_hstat)) != NULL)
	{
		int			fc_failures;

		/*
		 * 如果条目是新的，则本次不处理；它是新的。
		 * 注意“continue”跳过了循环底部的哈希删除调用。
		 */
		if (fc_entry->cycle_ctr == sync_cycle_ctr)
			continue;

		/* 否则断言我们没有遗漏它 */
		Assert((CycleCtr) (fc_entry->cycle_ctr + 1) == sync_cycle_ctr);

		/*
		 * 如果fsync关闭，则根本不必打开文件。
		 * （我们延迟检查直到这一点，以便动态更改fsync
		 * 的行为合理。）
		 */
		if (enableFsync)
		{
			/*
			 * 如果在检查点，我们希望不时吸收待处理请求，以防止
			 * fsync请求队列溢出。随意添加的条目是否会被
			 * hash_seq_search访问是未指定的，但我们不在乎，
			 * 因为我们反正不需要处理它们。
			 */
			if (--fc_absorb_counter <= 0)
			{
				AbsorbSyncRequests();
				fc_absorb_counter = FSYNCS_PER_ABSORB;
			}

			/*
			 * fsync表可能包含请求fsync段的请求，这些段在
			 * 我们到达它们时可能已被删除（unlink）。与其单纯
			 * 希望一个ENOENT（或Windows上的EACCES）错误可以被
			 * 忽略，不如我们在出错时吸收待处理请求然后重试。
			 * 由于mdunlink()在实际unlink之前排队“取消”消息，
			 * 因此，如果确实如此，fsync请求会在吸收后被标记为
			 * 取消。DROP DATABASE同样需要在开始删除之前告知
			 * 我们忘记fsync请求。
			 */
			for (fc_failures = 0; !fc_entry->canceled; fc_failures++)
			{
				char		fc_path[MAXPGPATH];

				INSTR_TIME_SET_CURRENT(fc_sync_start);
				if (syncsw[fc_entry->tag.handler].sync_syncfiletag(&fc_entry->tag,
																fc_path) == 0)
				{
					/* 成功；更新关于同步时机的统计信息 */
					INSTR_TIME_SET_CURRENT(fc_sync_end);
					fc_sync_diff = fc_sync_end;
					INSTR_TIME_SUBTRACT(fc_sync_diff, fc_sync_start);
					fc_elapsed = INSTR_TIME_GET_MICROSEC(fc_sync_diff);
					if (fc_elapsed > fc_longest)
						fc_longest = fc_elapsed;
					fc_total_elapsed += fc_elapsed;
					fc_processed++;

					if (log_checkpoints)
						elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms",
							 fc_processed,
							 fc_path,
							 (double) fc_elapsed / 1000);

					break;		/* 退出重试循环 */
				}

				/*
				 * 可能在fsync请求输入后，相应的关系已经被删除或
				 * 被截断。因此，允许ENOENT，但仅当我们在这个文件上
				 * 没有失败过。
				 */
				if (!FILE_POSSIBLY_DELETED(errno) || fc_failures > 0)
					ereport(data_sync_elevel(ERROR),
							(errcode_for_file_access(),
							 errmsg("could not fsync file \"%s\": %m",
									fc_path)));
				else
					ereport(DEBUG1,
							(errcode_for_file_access(),
							 errmsg_internal("could not fsync file \"%s\" but retrying: %m",
											 fc_path)));

				/*
				 * 吸收传入请求，并检查是否有取消
				 * 到达此关系分支。
				 */
				AbsorbSyncRequests();
				fc_absorb_counter = FSYNCS_PER_ABSORB; /* 也许可以... */
			}					/* 结束重试循环 */
		}

		/* 我们已经完成了这个条目，移除它 */
		if (hash_search(pendingOps, &fc_entry->tag, HASH_REMOVE, NULL) == NULL)
			elog(ERROR, "pendingOps corrupted");
	}							/* 结束对哈希表条目的循环 */

	/* 返回用于检查点结束的同步性能指标 */
	CheckpointStats.ckpt_sync_rels = fc_processed;
	CheckpointStats.ckpt_longest_sync = fc_longest;
	CheckpointStats.ckpt_agg_sync_time = fc_total_elapsed;

	/* 标记ProcessSyncRequests成功完成 */
	fc_sync_in_progress = false;
}

/*
 * RememberSyncRequest() -- 从同步请求的检查点端的回调
 *
 * 我们将fsync请求放入本地哈希表，以便在检查点下次执行时处理。
 * UNLINK请求进入一个单独的链表，因为它们单独处理。
 *
 * 有关支持的同步请求类型的更多信息，请参见sync.h。
 */
void RememberSyncRequest(const FileTag *fc_ftag, SyncRequestType fc_type)
{
	Assert(pendingOps);

	if (fc_type == SYNC_FORGET_REQUEST)
	{
		PendingFsyncEntry *fc_entry;

		/* 取消之前输入的请求 */
		fc_entry = (PendingFsyncEntry *) hash_search(pendingOps,
												  (void *) fc_ftag,
												  HASH_FIND,
												  NULL);
		if (fc_entry != NULL)
			fc_entry->canceled = true;
	}
	else if (fc_type == SYNC_FILTER_REQUEST)
	{
		HASH_SEQ_STATUS fc_hstat;
		PendingFsyncEntry *fc_entry;
		ListCell   *fc_cell;

		/* 取消匹配的fsync请求 */
		hash_seq_init(&fc_hstat, pendingOps);
		while ((fc_entry = (PendingFsyncEntry *) hash_seq_search(&fc_hstat)) != NULL)
		{
			if (fc_entry->tag.handler == fc_ftag->handler &&
				syncsw[fc_ftag->handler].sync_filetagmatches(fc_ftag, &fc_entry->tag))
				fc_entry->canceled = true;
		}

		/* 取消匹配的unlink请求 */
		foreach(fc_cell, pendingUnlinks)
		{
			PendingUnlinkEntry *fc_entry = (PendingUnlinkEntry *) lfirst(fc_cell);

			if (fc_entry->tag.handler == fc_ftag->handler &&
				syncsw[fc_ftag->handler].sync_filetagmatches(fc_ftag, &fc_entry->tag))
				fc_entry->canceled = true;
		}
	}
	else if (fc_type == SYNC_UNLINK_REQUEST)
	{
		/* Unlink请求：将其放入链表中 */
		MemoryContext fc_oldcxt = MemoryContextSwitchTo(pendingOpsCxt);
		PendingUnlinkEntry *fc_entry;

		fc_entry = palloc(sizeof(PendingUnlinkEntry));
		fc_entry->tag = *fc_ftag;
		fc_entry->cycle_ctr = checkpoint_cycle_ctr;
		fc_entry->canceled = false;

		pendingUnlinks = lappend(pendingUnlinks, fc_entry);

		MemoryContextSwitchTo(fc_oldcxt);
	}
	else
	{
		/* 正常情况：输入请求以fsync该段 */
		MemoryContext fc_oldcxt = MemoryContextSwitchTo(pendingOpsCxt);
		PendingFsyncEntry *fc_entry;
		bool		fc_found;

		Assert(fc_type == SYNC_REQUEST);

		fc_entry = (PendingFsyncEntry *) hash_search(pendingOps,
												  (void *) fc_ftag,
												  HASH_ENTER,
												  &fc_found);
		/* 如果是新条目，或者之前被取消，进行初始化 */
		if (!fc_found || fc_entry->canceled)
		{
			fc_entry->cycle_ctr = sync_cycle_ctr;
			fc_entry->canceled = false;
		}

		

/*
		 * 注意：如果条目已经存在，我们故意不改变 cycle_ctr。cycle_ctr
		 * 必须代表可以在条目中找到的最旧的 fsync 请求。
		 */

		MemoryContextSwitchTo(fc_oldcxt);
	}
}

/*
 * 在本地注册同步请求，或者将其转发给检查点处理程序。
 *
 * 如果 retryOnError 为 true，当队列中没有空间时，我们将持续尝试。
 * 如果成功则返回 true，若没有空间则返回 false。
 */
bool RegisterSyncRequest(const FileTag *fc_ftag, SyncRequestType fc_type,
					bool fc_retryOnError)
{
	bool		fc_ret;

	if (pendingOps != NULL)
	{
		
/* 独立后端或启动进程：fsync 状态是本地的 */
		RememberSyncRequest(fc_ftag, fc_type);
		return true;
	}

	for (;;)
	{
		/*
		 * 通知检查点关于它的情况。如果我们在 retryOnError 模式下未能排队消息，
		 * 我们必须睡眠并重试……丑陋，但希望不会经常发生。
		 *
		 * XXX 我们应该在这个循环中 CHECK_FOR_INTERRUPTS 吗？
		 * 在 SYNC_UNLINK_REQUEST 的情况下出错将导致不再使用的文件仍然
		 * 存在于磁盘上，这将是糟糕的，因此我倾向于假设检查点将始终快速
		 * 清空队列。
		 */
		fc_ret = ForwardSyncRequest(fc_ftag, fc_type);

		/*
		 * 如果我们成功排队请求，或者我们失败并被指示不要在错误时重试，退出。
		 */
		if (fc_ret || (!fc_ret && !fc_retryOnError))
			break;

		WaitLatch(NULL, WL_EXIT_ON_PM_DEATH | WL_TIMEOUT, 10,
				  WAIT_EVENT_REGISTER_SYNC_REQUEST);
	}

	return fc_ret;
}
